package com.mojavelinux.open18;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import javax.persistence.Version;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.hibernate.Hibernate;
import org.hibernate.validator.NotNull;
import org.hibernate.validator.Range;

/**
 * <p>
 * Represents a tee box for an individual hole and tee set (blue, white, red,
 * etc.).
 * </p>
 * <p>
 * This class is an example of a One-to-Many relationship that is mapped to
 * overlapping primary and foreign keys. When this relationship exists, one of
 * the two definitions must be marked as read-only (insertable=false,
 * updatable=false) using either a Column or JoinColumn annotation. These
 * attributes provide a hint as to which relationship manages the value of the
 * key.
 * </p>
 */
@Entity
@Table( name = "tee", uniqueConstraints = @UniqueConstraint( columnNames = { "tee_set_id", "hole_id" } ) )
public class Tee implements Serializable, Comparable<Tee> {

	private static final long serialVersionUID = 1L;

	private CompositeId id;
	
	private int version = 0;

	private TeeSet teeSet;

	private Hole hole;

	private Integer distanceToGreen;

	public Tee() {}
	
	public Tee( TeeSet teeSet, Hole hole, Integer distance ) {
		this.teeSet = teeSet;
		this.hole = hole;
		this.distanceToGreen = distance;
	}
	
	// An alternative method to @EmbeddedId is to use @IdClass and multiple
	// @Id annotated properties in this class to represent the fields that
	// compose the key.
	@EmbeddedId
	public CompositeId getId() {
		return id;
	}

	public void setId( CompositeId id ) {
		this.id = id;
	}

	@Version
	@Column( name = "version" )
	public int getVersion() {
		return version;
	}
	
	protected void setVersion( int version ) {
		this.version = version;
	}

	@ManyToOne( optional = false )
	@NotNull
	@JoinColumn( name = "tee_set_id", insertable = false, updatable = false )
	public TeeSet getTeeSet() {
		return teeSet;
	}

	public void setTeeSet( TeeSet tee ) {
		this.teeSet = tee;
	}

	@ManyToOne( optional = false )
	@NotNull
	@JoinColumn( name = "hole_id", insertable = false, updatable = false )
	public Hole getHole() {
		return hole;
	}

	public void setHole( Hole hole ) {
		this.hole = hole;
	}

	/**
	 * The distance as measured along the centerline of a given hole from any
	 * tee center point to the green center point.
	 */
	@Column( name = "distance" )
	@NotNull
	@Range( min = 1, max = 999 )
	public Integer getDistanceToGreen() {
		return this.distanceToGreen;
	}

	public void setDistanceToGreen( Integer distance ) {
		this.distanceToGreen = distance;
	}

	/**
	 * <p>Compares two tees by considering both the position of the tee box and
	 * the hole number on the course.  Lower numbers are of a higher order.</p>
	 * <p><b>Important:</b> This comparator can be a bit dangerous if the TeeSet
	 * and Hole are not loaded from persistence storage in an eager fetch.  In that
	 * case, the fetch is going to happen at an inopportune time and lead to lots
	 * of extra queries.  Aside from fetching these objects eagerly, an optimization
	 * could be done in the database to replicate the properties (tee position and hole id)
	 * needed for sorting.</p>
	 * QUESTION: should I use Hibernate#isInitialized() here?
	 */
	public int compareTo( Tee other ) {
		if ( this.teeSet != null && other.getTeeSet() != null && !this.teeSet.equals( other.getTeeSet() )) {
			return this.teeSet.compareTo( other.getTeeSet() );
		}
		
		if ( this.hole == null ) {
			return other.getHole() == null ? 0 : -1;
		}
		else {
			return this.hole.compareTo( other.getHole() );
		}
	}
	
	public int hashCode() {
		final int PRIME = 31;
		int result = 1;
		result = PRIME * result + id.hashCode();
		// Hibernate#isInitialized() return true if not a proxy
		if ( Hibernate.isInitialized( teeSet ) ) {
			result = PRIME * result + ( ( teeSet == null ) ? 0 : teeSet.hashCode() );
		}
		
		// Hibernate#isInitialized() return true if not a proxy
		if ( Hibernate.isInitialized( hole ) ) {
			result = PRIME * result + ( ( hole == null ) ? 0 : hole.hashCode() );
		}
		
		result = PRIME * result + ( ( distanceToGreen == null ) ? 0 : distanceToGreen.hashCode() );
		return result;
	}

	/**
	 * Two Tee objects are considered equal either if their composite ids
	 * are equivalent (same tee_set_id and hole_id). If a composite id has not been
	 * established, then the two objects are equal if both the tee and hole
	 * references are equivalent.  Distance plays no part in the comparison.
	 */
	public boolean equals( Object o ) {
		if ( this == o ) {
			return true;
		}
		
		if ( !( o instanceof Tee ) ) {
			return false;
		}
		
		final Tee other = (Tee) o;
		
		// short circuit by comparing ids
		if ( id != null && other.getId() != null ) {
			return id.equals( other.getId() );
		}
		
		// Hibernate#isInitialized() return true if not a proxy
		if ( Hibernate.isInitialized( teeSet ) ) {
			if ( teeSet == null ) {
				if ( other.teeSet != null ) {
					return false;
				}
			}
			else if ( !teeSet.equals( other.teeSet ) ) {
				return false;
			}
		}
		
		// Hibernate#isInitialized() return true if not a proxy
		if ( Hibernate.isInitialized( hole ) ) {
			if ( hole == null ) {
				if ( other.hole != null ) {
					return false;
				}
			}
			else if ( !hole.equals( other.hole ) ) {
				return false;
			}
		}
		
		return true;
	}
	
	public String toString() {
		ToStringBuilder builder = new ToStringBuilder( this, ToStringStyle.MULTI_LINE_STYLE );
		if ( id != null ) {
			builder.append( "tee_set_id", id.getTeeSetId() );
			builder.append( "hole_id", id.getHoleId() );
		}
		
		if ( teeSet != null && Hibernate.isInitialized( teeSet ) ) {
			builder.append( "teeSet", teeSet.toString() );
		}
	
		if ( hole != null && Hibernate.isInitialized( hole ) ) {
			builder.append( "hole", hole.toString() );
		}
		
		builder.append( "distance", distanceToGreen );
		return builder.toString();
	}

	@Embeddable
	public static class CompositeId implements Serializable {

		private static final long serialVersionUID = 1L;

		private long teeSetId;

		private long holeId;

		@Column( name = "hole_id" )
		public long getHoleId() {
			return holeId;
		}

		public void setHoleId( long holeId ) {
			this.holeId = holeId;
		}

		@Column( name = "tee_set_id" )
		public long getTeeSetId() {
			return teeSetId;
		}

		public void setTeeSetId( long teeSetId ) {
			this.teeSetId = teeSetId;
		}

		public int hashCode() {
			final int PRIME = 31;
			int result = 1;
			result = PRIME * result + (int) ( holeId ^ ( holeId >>> 32 ) );
			result = PRIME * result + (int) ( teeSetId ^ ( teeSetId >>> 32 ) );
			return result;
		}

		public boolean equals( Object o ) {
			if ( this == o ) {
				return true;
			}

			if ( !( o instanceof CompositeId ) ) {
				return false;
			}

			final CompositeId other = (CompositeId) o;
			if ( holeId != other.holeId ) {
				return false;
			}

			if ( teeSetId != other.teeSetId ) {
				return false;
			}

			return true;
		}

	}

}
